/*
 * Copyright 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.compose.ui.text.font

import androidx.collection.LruCache
import androidx.compose.runtime.State
import androidx.compose.ui.text.platform.makeSynchronizedObject
import androidx.compose.ui.text.platform.synchronized
import androidx.compose.ui.util.fastMap

internal class FontFamilyResolverImpl(
    internal val platformFontLoader: PlatformFontLoader /* exposed for desktop ParagraphBuilder */,
    private val platformResolveInterceptor: PlatformResolveInterceptor =
        PlatformResolveInterceptor.Default,
    private val typefaceRequestCache: TypefaceRequestCache = GlobalTypefaceRequestCache,
    private val fontListFontFamilyTypefaceAdapter: FontListFontFamilyTypefaceAdapter =
        FontListFontFamilyTypefaceAdapter(GlobalAsyncTypefaceCache),
    private val platformFamilyTypefaceAdapter: PlatformFontFamilyTypefaceAdapter =
        PlatformFontFamilyTypefaceAdapter(),
) : FontFamily.Resolver {
    private val createDefaultTypeface: (TypefaceRequest) -> Any = {
        resolve(it.copy(fontFamily = null)).value
    }

    override suspend fun preload(fontFamily: FontFamily) {
        // all other types of FontFamily are already preloaded.
        if (fontFamily !is FontListFontFamily) return

        fontListFontFamilyTypefaceAdapter.preload(fontFamily, platformFontLoader)

        val typeRequests =
            fontFamily.fonts.fastMap {
                TypefaceRequest(
                    platformResolveInterceptor.interceptFontFamily(fontFamily),
                    platformResolveInterceptor.interceptFontWeight(it.weight),
                    platformResolveInterceptor.interceptFontStyle(it.style),
                    FontSynthesis.All,
                    platformFontLoader.cacheKey,
                )
            }

        typefaceRequestCache.preWarmCache(typeRequests) { typeRequest ->
            @Suppress("MoveLambdaOutsideParentheses")
            fontListFontFamilyTypefaceAdapter.resolve(
                typefaceRequest = typeRequest,
                platformFontLoader = platformFontLoader,
                onAsyncCompletion = { /* nothing */ },
                createDefaultTypeface = createDefaultTypeface,
            )
                ?: platformFamilyTypefaceAdapter.resolve(
                    typefaceRequest = typeRequest,
                    platformFontLoader = platformFontLoader,
                    onAsyncCompletion = { /* nothing */ },
                    createDefaultTypeface = createDefaultTypeface,
                )
                ?: throw IllegalStateException("Could not load font")
        }
    }

    override fun resolve(
        fontFamily: FontFamily?,
        fontWeight: FontWeight,
        fontStyle: FontStyle,
        fontSynthesis: FontSynthesis,
    ): State<Any> {
        return resolve(
            TypefaceRequest(
                platformResolveInterceptor.interceptFontFamily(fontFamily),
                platformResolveInterceptor.interceptFontWeight(fontWeight),
                platformResolveInterceptor.interceptFontStyle(fontStyle),
                platformResolveInterceptor.interceptFontSynthesis(fontSynthesis),
                platformFontLoader.cacheKey,
            )
        )
    }

    /** Resolves the final [typefaceRequest] without interceptors. */
    private fun resolve(typefaceRequest: TypefaceRequest): State<Any> {
        val result =
            typefaceRequestCache.runCached(typefaceRequest) { onAsyncCompletion ->
                fontListFontFamilyTypefaceAdapter.resolve(
                    typefaceRequest,
                    platformFontLoader,
                    onAsyncCompletion,
                    createDefaultTypeface,
                )
                    ?: platformFamilyTypefaceAdapter.resolve(
                        typefaceRequest,
                        platformFontLoader,
                        onAsyncCompletion,
                        createDefaultTypeface,
                    )
                    ?: throw IllegalStateException("Could not load font")
            }
        return result
    }
}

/**
 * Platform level [FontFamily.Resolver] argument interceptor. This interface is intended to bridge
 * accessibility constraints on any platform with Compose through the use of
 * [FontFamilyResolverImpl.resolve].
 */
internal interface PlatformResolveInterceptor {

    fun interceptFontFamily(fontFamily: FontFamily?): FontFamily? = fontFamily

    fun interceptFontWeight(fontWeight: FontWeight): FontWeight = fontWeight

    fun interceptFontStyle(fontStyle: FontStyle): FontStyle = fontStyle

    fun interceptFontSynthesis(fontSynthesis: FontSynthesis): FontSynthesis = fontSynthesis

    companion object {
        // NO-OP default interceptor
        internal val Default: PlatformResolveInterceptor = object : PlatformResolveInterceptor {}
    }
}

internal val GlobalTypefaceRequestCache = TypefaceRequestCache()
internal val GlobalAsyncTypefaceCache = AsyncTypefaceCache()

internal expect class PlatformFontFamilyTypefaceAdapter() : FontFamilyTypefaceAdapter {
    override fun resolve(
        typefaceRequest: TypefaceRequest,
        platformFontLoader: PlatformFontLoader,
        onAsyncCompletion: (TypefaceResult.Immutable) -> Unit,
        createDefaultTypeface: (TypefaceRequest) -> Any,
    ): TypefaceResult?
}

internal data class TypefaceRequest(
    val fontFamily: FontFamily?,
    val fontWeight: FontWeight,
    val fontStyle: FontStyle,
    val fontSynthesis: FontSynthesis,
    val resourceLoaderCacheKey: Any?,
)

internal sealed interface TypefaceResult : State<Any> {
    val cacheable: Boolean

    // Immutable results present as State, but don't trigger a read observer
    class Immutable(override val value: Any, override val cacheable: Boolean = true) :
        TypefaceResult

    class Async(internal val current: AsyncFontListLoader) : TypefaceResult, State<Any> by current {
        override val cacheable: Boolean
            get() = current.cacheable
    }
}

internal class TypefaceRequestCache {
    internal val lock = makeSynchronizedObject()
    // @GuardedBy("lock")
    private val resultCache = LruCache<TypefaceRequest, TypefaceResult>(16)

    fun runCached(
        typefaceRequest: TypefaceRequest,
        resolveTypeface: ((TypefaceResult) -> Unit) -> TypefaceResult,
    ): State<Any> {
        synchronized(lock) {
            resultCache[typefaceRequest]?.let {
                if (it.cacheable) {
                    return it
                } else {
                    resultCache.remove(typefaceRequest)
                }
            }
        }
        // this is not run synchronized2 as it incurs expected file system reads.
        //
        // As a result, it is possible the same FontFamily resolution is started twice if this
        // function is entered concurrently. This is explicitly allowed, to avoid creating a global
        // lock here.
        //
        // This function must ensure that the final result is a valid cache in the presence of
        // multiple entries.
        //
        // Necessary font load de-duping is the responsibility of actual font resolution mechanisms.
        val currentTypefaceResult =
            try {
                resolveTypeface { finalResult ->
                    // may this after runCached returns, or immediately if the typeface is
                    // immediately
                    // available without dispatch

                    // this converts an async (state) result to an immutable (val) result to
                    // optimize
                    // future lookups
                    synchronized(lock) {
                        if (finalResult.cacheable) {
                            resultCache.put(typefaceRequest, finalResult)
                        } else {
                            resultCache.remove(typefaceRequest)
                        }
                    }
                }
            } catch (cause: Exception) {
                throw IllegalStateException("Could not load font", cause)
            }
        synchronized(lock) {
            // async result may have completed prior to this block entering, do not overwrite
            // final results
            if (resultCache[typefaceRequest] == null && currentTypefaceResult.cacheable) {
                resultCache.put(typefaceRequest, currentTypefaceResult)
            }
        }
        return currentTypefaceResult
    }

    fun preWarmCache(
        typefaceRequests: List<TypefaceRequest>,
        resolveTypeface: (TypefaceRequest) -> TypefaceResult,
    ) {
        for (i in typefaceRequests.indices) {
            val typeRequest = typefaceRequests[i]

            val prior = synchronized(lock) { resultCache[typeRequest] }
            if (prior != null) continue

            val next =
                try {
                    resolveTypeface(typeRequest)
                } catch (cause: Exception) {
                    throw IllegalStateException("Could not load font", cause)
                }

            // only cache immutable, should not reach as FontListFontFamilyTypefaceAdapter already
            // has async fonts in permanent cache
            if (next is TypefaceResult.Async) continue

            synchronized(lock) { resultCache.put(typeRequest, next) }
        }
    }

    // @VisibleForTesting
    internal fun get(typefaceRequest: TypefaceRequest) =
        synchronized(lock) { resultCache.get(typefaceRequest) }

    // @VisibleForTesting
    internal val size: Int
        get() = synchronized(lock) { resultCache.size() }
}
